package ex3.visitors;

import ic.ast.Visitor;
import ic.ast.decl.*;
import ic.ast.expr.*;
import ic.ast.stmt.*;
import ic.symbols.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
 * Created with IntelliJ IDEA.
 * User: Roi
 * Date: 12/6/13
 */
public class REPL implements Visitor {

    private State state = new State();
    private String className = null;
    private String methodName = null;
    private String[] methodParameters = null;

    public REPL(String className, String methodName, String[] params) {
        this.className = className;
        this.methodName = methodName;
        this.methodParameters = params;
    }

    @Override
    public Object visit(Program program) throws Exception {
        Object value = null;
        List<DeclClass> classes = program.getClasses();

        for (DeclClass dc : classes) {
            if (dc.getName().equals(className)) {
                value = dc.accept(this);
                break;
            }
        }

        if (value != null) {
            try {
                Object[] arr = state.arrLookup((String) value);
                for (Object anArr : arr) {
                    System.out.println(anArr);
                }
            } catch (Exception e) {
                System.out.println(value);
            }
        }

        return null;
    }

    @Override
    public Object visit(DeclClass icClass) throws Exception {
        List<DeclMethod> methods = icClass.getMethods();
        for (DeclMethod method : methods) {
            if (method.getName().equals(methodName)) {
                return method.accept(this);
            }
        }
        return null;
    }

    @Override
    public Object visit(DeclField field) {
        return null;
    }

    @Override
    public Object visit(DeclVirtualMethod method) {
        return null;
    }

    @Override
    public Object visit(DeclStaticMethod method) throws Exception {
        state.globals.put(method.getName(), method);

        ActivationRecord ar = new ActivationRecord();
        Object returnStatement = null;
        List<Parameter> param = method.getFormals();

        for (int i = 0; i < param.size(); i++) {
            try {
                String typeStr = param.get(i).getType().getDisplayName();
                switch (typeStr) {
                    case "int":
                        ar.values.put(param.get(i).getName(), Integer.parseInt(methodParameters[i]));
                        break;
                    case "boolean":
                        ar.values.put(param.get(i).getName(), Boolean.parseBoolean(methodParameters[i]));
                        break;
                    default:
                        ar.values.put(param.get(i).getName(), methodParameters[i]);
                        break;
                }
            } catch (Exception e) {
                throw new Exception("Error: visit DeclStaticMethod");
            }
        }

        state.a_stack.push(ar);

        for (Statement stmt : method.getStatements()) {
            returnStatement = stmt.accept(this);
        }

        return returnStatement;
    }

    @Override
    public Object visit(DeclLibraryMethod method) {
        return null;
    }

    @Override
    public Object visit(Parameter formal) throws Exception {
        return formal.getType().accept(this);
    }

    @Override
    public Object visit(PrimitiveType type) {
        return null;
    }

    @Override
    public Object visit(ClassType type) {
        return null;
    }

    @Override
    public Object visit(StmtAssignment assignment) throws Exception {
        int index = 0;
        Ref ref = assignment.getVariable();
        Expression expression = assignment.getAssignment();
        String name = null;

        if (ref instanceof RefVariable) {
            name = ((RefVariable) ref).getName();
        }
        else if (ref instanceof RefArrayElement) {
            RefArrayElement refArrayElement = (RefArrayElement) ref;
            name = ((RefVariable) refArrayElement.getArray()).getName();
            index = (Integer) refArrayElement.getIndex().accept(this);
        }

        if (expression instanceof NewArray) {
            int size = (Integer) expression.accept(this);
            state.initArr(name, size);
        } else if (!state.lookupAndSet(name, index, expression.accept(this))) {
            throw new Exception("Error: visit StmtAssignment");
        }

        return null;
    }

    @Override
    public Object visit(StmtCall callStatement) {
        return null;
    }

    @Override
    public Object visit(StmtReturn returnStatement) throws Exception {
        if (!returnStatement.hasValue()) {
            return null;
        }
        return returnStatement.getValue().accept(this);
    }

    @Override
    public Object visit(StmtIf ifStatement) throws Exception {
        if (ifStatement.hasElse()) {
            if ((Boolean) ifStatement.getCondition().accept(this)) {
                return ifStatement.getOperation().accept(this);
            } else {
                return ifStatement.getElseOperation().accept(this);
            }
        } else {
            if ((Boolean) ifStatement.getCondition().accept(this)) {
                return ifStatement.getOperation().accept(this);
            }
        }

        return null;
    }

    @Override
    public Object visit(StmtWhile whileStatement) throws Exception {
        Statement curr = null;

        while ((Boolean) whileStatement.getCondition().accept(this)) {
            curr = (Statement) whileStatement.getOperation().accept(this);
            if (curr instanceof StmtBreak) {
                break;
            }
            if (curr instanceof StmtContinue) {
            }
        }

        return curr;
    }

    @Override
    public Object visit(StmtBreak breakStatement) {
        return new StmtBreak(0);
    }

    @Override
    public Object visit(StmtContinue continueStatement) {
        return new StmtContinue(0);
    }

    @Override
    public Object visit(StmtBlock statementsBlock) throws Exception {
        Object curr;

        for (Statement s : statementsBlock.getStatements()) {
            curr = s.accept(this);
            if (curr instanceof StmtBreak) {
                return curr;
            }
            if (curr instanceof StmtContinue) {
                return curr;
            }
        }

        return null;
    }

    @Override
    public Object visit(LocalVariable localVariable) throws Exception {
        String name = localVariable.getName();

        if (localVariable.isInitialized()) {
            Expression exp = localVariable.getInitialValue();
            if (exp instanceof NewArray) {
                int size = (Integer) exp.accept(this);
                state.initArr(name, size);
            } else {
                state.set(name, 0, exp.accept(this));
                return exp.accept(this);
            }
        } else {
            state.set(name, 0, null);
        }

        return null;
    }

    @Override
    public Object visit(RefVariable location) throws Exception {
        String key = location.getName();
        Object obj;

        try {
            obj = state.lookup(key);
        } catch (Exception e) {
            throw new Exception("Error: visit RefVariable");
        }

        if (obj instanceof Integer || obj instanceof Boolean || obj instanceof String) {
            return obj;
        } else {
            return null;
        }
    }

    @Override
    public Object visit(RefField location) {
        return null;
    }

    @Override
    public Object visit(RefArrayElement location) throws Exception {
        Object[] obj;
        String arrName = (String) location.getArray().accept(this);
        int index = (Integer) location.getIndex().accept(this);

        try {
            obj = state.arrLookup(arrName);
        } catch (Exception e) {
            throw new Exception("Error: visit RefArrayElement");
        }

        if (obj[index] instanceof Integer) {
            return obj[index];
        } else if (obj[index] instanceof Boolean) {
            return obj[index];
        } else if (obj[index] instanceof String) {
            return obj[index];
        } else {
            return null;
        }
    }

    @Override
    public Object visit(StaticCall call) {
        return null;
    }

    @Override
    public Object visit(VirtualCall call) {
        return null;
    }

    @Override
    public Object visit(This thisExpression) {
        return null;
    }

    @Override
    public Object visit(NewInstance newClass) {
        return null;
    }

    @Override
    public Object visit(NewArray newArray) throws Exception {
        return newArray.getSize().accept(this);
    }

    @Override
    public Object visit(Length length) throws Exception {
        String name = (String) length.getArray().accept(this);
        try {
            Object[] arr = state.arrLookup(name);
            return arr.length;
        } catch (Exception e) {
            throw new Exception("Error: visit Length");
        }
    }

    @Override
    public Object visit(Literal literal) {
        switch (literal.getType()) {
            case INT:
                return Integer.parseInt((String) literal.getValue());
            case BOOLEAN:
                return Boolean.parseBoolean((String) literal.getValue());
            case STRING:
                return literal.getValue();
            case VOID:
                return null;
            default:
                return null;
        }
    }

    @Override
    public Object visit(UnaryOp unaryOp) throws Exception {
        switch (unaryOp.getOperator()) {
            case UMINUS:
                return -(Integer) unaryOp.accept(this);
            case LNEG:
                return !(Boolean) unaryOp.accept(this);
            default:
                return null;
        }
    }

    @Override
    public Object visit(BinaryOp binaryOp) throws Exception {
        switch (binaryOp.getOperator()) {
            case PLUS:
                try {
                    return (Integer) binaryOp.getFirstOperand().accept(this) +
                            (Integer) binaryOp.getSecondOperand().accept(this);
                } catch (ClassCastException e) {
                    return (String) binaryOp.getFirstOperand().accept(this) +
                            (String) binaryOp.getSecondOperand().accept(this);
                }
            case MINUS:
                return (Integer) binaryOp.getFirstOperand().accept(this) -
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case MULTIPLY:
                return (Integer) binaryOp.getFirstOperand().accept(this) *
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case DIVIDE:
                return (Integer) binaryOp.getFirstOperand().accept(this) /
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case MOD:
                return (Integer) binaryOp.getFirstOperand().accept(this) %
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case LAND:
                return (Boolean) binaryOp.getFirstOperand().accept(this) &&
                        (Boolean) binaryOp.getSecondOperand().accept(this);
            case LOR:
                return (Boolean) binaryOp.getFirstOperand().accept(this) ||
                        (Boolean) binaryOp.getSecondOperand().accept(this);
            case LT:
                return (Integer) binaryOp.getFirstOperand().accept(this) <
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case LTE:
                return (Integer) binaryOp.getFirstOperand().accept(this) <=
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case GT:
                return (Integer) binaryOp.getFirstOperand().accept(this) >
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case GTE:
                return (Integer) binaryOp.getFirstOperand().accept(this) >=
                        (Integer) binaryOp.getSecondOperand().accept(this);
            case EQUAL:
                return binaryOp.getFirstOperand().accept(this) ==
                        binaryOp.getSecondOperand().accept(this);
            case NEQUAL:
                return binaryOp.getFirstOperand().accept(this) !=
                        binaryOp.getSecondOperand().accept(this);
            default:
                return null;
        }
    }

    @Override
    public Object visit(GlobalSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(ClassSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(BlockSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(MethodSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(SymbolTable symbolTable) {
        return null;
    }

    /**
     * Helper class - activation record
     */
    private class ActivationRecord {
        Map<String, Object> values = new HashMap<String, Object>();
        Map<String, Object[]> arrValues = new HashMap<String, Object[]>();

        @Override
        public String toString() {
            return "";
        }
    }

    /**
     * Helper class - state
     */
    private class State {
        Stack<ActivationRecord> a_stack = new Stack<ActivationRecord>();
        Map<String, DeclMethod> globals = new HashMap<String, DeclMethod>();

        State() {
            a_stack.push(new ActivationRecord());
        }

        boolean lookupAndSet(String key, int index, Object value) {
            ActivationRecord ar = a_stack.peek();
            if (ar.arrValues.containsKey(key)) {
                Object[] arr = ar.arrValues.get(key);
                arr[index] = value;
                return true;
            } else if (ar.values.containsKey(key)) {
                ar.values.put(key, value);
                return true;
            }
            return false;
        }

        void set(String key, int index, Object value) {
            ActivationRecord ar = a_stack.peek();
            if (ar.arrValues.containsKey(key)) {
                Object[] arr = ar.arrValues.get(key);
                arr[index] = value;
            } else {
                ar.values.put(key, value);
            }
        }

        void initArr(String key, int size) {
            ActivationRecord ar = a_stack.peek();
            ar.arrValues.put(key, new Object[size]);
            ar.values.put(key, key);
        }

        Object lookup(String key) throws Exception {
            ActivationRecord ar = a_stack.peek();
            if (ar.values.containsKey(key)) {
                return ar.values.get(key);
            } else {
                throw new Exception();
            }
        }

        Object[] arrLookup(String key) throws Exception {
            ActivationRecord ar = a_stack.peek();
            if (ar.arrValues.containsKey(key)) {
                return ar.arrValues.get(key);
            } else {
                throw new Exception();
            }
        }

        @Override
        public String toString() {
            return a_stack.peek().toString();
        }
    }
}
